using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using MvvmFx.Common.ViewModels.Behaviors.Observable;

namespace MvvmFx.Common.ViewModels.Behaviors.ChangeTracking
{
    public class ChangeService : ViewModelBase,
                                 IObservable<ChangeService>
    {
        #region Instance variables

        private readonly ViewModelBase _viewModel;
        private bool _isModified;

        private readonly TrackingMode _trackingMode;
        private readonly Dictionary<string, IChangeTrackable> _trackedChildren;
        private readonly Dictionary<ChangeService, PropertyObserver<ChangeService>> _childObservers;

        #endregion

        #region Constructors

        public ChangeService(ViewModelBase viewModel)
            : this(viewModel, TrackingMode.IgnoreChildren)
        {
        }

        public ChangeService(ViewModelBase viewModel, TrackingMode trackingMode)
        {
            _viewModel = viewModel;
            _trackingMode = trackingMode;

            // Track the property changed event of the viewModel, 
            // and signal in the ChangeService when it's modified.
            _viewModel.PropertyChanged += ViewModelPropertyChanged;

            // If this ChangeService should track children,
            // iterate through the properties of the parent ViewModel, and look for
            // any child properties that implement IChangeTrackable.
            //
            if (_trackingMode == TrackingMode.TrackChildren)
            {
                _childObservers = new Dictionary<ChangeService, PropertyObserver<ChangeService>>();
                _trackedChildren = new Dictionary<string, IChangeTrackable>();

                PropertyInfo[] propinfos = viewModel.GetType().GetProperties();
                foreach (PropertyInfo propinfo in propinfos)
                {
                    string propertyName = propinfo.Name;

                    if (typeof(IChangeTrackable).IsAssignableFrom(propinfo.PropertyType))
                    {
                        // Add the propertyName as a key to the dictionary of tracked properties.
                        // When the PropertyChanged event of the ViewModel is raised, the ViewModelPropertyChanged
                        // method will check for this key and call the ViewModelTrackableChildPropertyChanged method if found.
                        //
                        _trackedChildren.Add(propertyName, null);
                    }
                }
            }
        }

        #endregion

        private void ViewModelTrackableChildPropertyChanged(object sender, EventArgs e)
        {
            var propArgs = (PropertyChangedEventArgs)e;
            string propertyName = propArgs.PropertyName;

            // Remove the property changed notification from the old value
            IChangeTrackable trackedChild;
            if (_trackedChildren.TryGetValue(propertyName, out trackedChild))
            {
                if (trackedChild != null)
                {
                    _childObservers[trackedChild.ChangeService].RemoveHandler("IsModified");
                    _childObservers.Remove(trackedChild.ChangeService);
                    _trackedChildren[propertyName] = null;
                }
            }

            // Get the source value
            PropertyInfo propinfo = _viewModel.GetType().GetProperty(propertyName);
            object propvalue = propinfo.GetValue(_viewModel, null);

            if (propvalue is IChangeTrackable)
            {
                // Get the new object value of the IChangeTrackable property
                var trackableChild = propvalue as IChangeTrackable;

                // Get a property observer for its ChangeService
                var childPropertyObserver = trackableChild.ChangeService.NewPropertyObserver();

                // Add a handler to changes in the IsModified property
                childPropertyObserver.AddHandler("IsModified", ChildIsModifiedChanged);

                // Store the child PropertyObserver and the object
                _childObservers[trackableChild.ChangeService] = childPropertyObserver;
                _trackedChildren[propertyName] = trackableChild;
            }
        }

        private void ChildIsModifiedChanged(ChangeService trackedChild)
        {
            if (trackedChild.IsModified)
                MarkModified();
        }

        /// <summary>
        /// Event handler for the PropertyChanged event of the ViewModel
        /// object being tracked. This marks the ChangeService as being
        /// modified, and maintains hooks to a trackable child object if
        /// the TrackingMode is TrackChildren.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (_trackingMode == TrackingMode.TrackChildren)
            {
                if (_trackedChildren.ContainsKey(e.PropertyName))
                {
                    ViewModelTrackableChildPropertyChanged(sender, e);
                }
            }
            MarkModified();
        }

        /// <summary>
        /// Mark the object as being modified
        /// </summary>
        public virtual void MarkModified()
        {
            if (!_isModified)
            {
                _isModified = true;
                OnPropertyChanged("IsModified");
            }
        }

        /// <summary>
        /// Marks the object as not being modified
        /// </summary>
        public virtual void MarkUnmodified()
        {
            if (_isModified)
            {
                _isModified = false;
                OnPropertyChanged("IsModified");

                // If the tracking mode is set to track children,
                // mark the children as being Unmodified too. 
                // This ensure future changes to them change them back to IsModified 
                // which gets passed upstream here.
                //
                if (_trackingMode == TrackingMode.TrackChildren)
                {
                    foreach (var child in _trackedChildren.Values)
                    {
                        child.ChangeService.MarkUnmodified();
                    }
                }
            }
        }

        /// <summary>
        /// Gets whether the page has been modified since it was loaded
        /// </summary>
        public bool IsModified
        {
            get
            {
                return _isModified;
            }
        }

        /// <summary>
        /// Event handler for marking the object as modified.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void DoMarkModified(object sender, EventArgs e)
        {
            MarkModified();
        }

        #region IObservable Members

        /// <summary>
        /// Return a new property observer for registering for
        /// property change notifcations on this object.
        /// </summary>
        /// <returns></returns>
        public PropertyObserver<ChangeService> NewPropertyObserver()
        {
            return new PropertyObserver<ChangeService>(this);
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// If to dispose state field.
        /// </summary>
        private bool _disposed;

        public void Dispose()
        {
            Dispose(true);

            // Use SupressFinalize in case a subclass
            // of this type implements a finalizer.
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            // If you need thread safety, use a lock around these 
            // operations, as well as in your methods that use the resource.
            if (!_disposed)
            {
                if (disposing)
                {
                    if (_viewModel != null)
                        _viewModel.PropertyChanged -= ViewModelPropertyChanged;
                }

                // Indicate that the instance has been disposed.
                _disposed = true;
            }
        }

        #endregion
    }
}